package edu.kit.ksri.db.mturk.plugins.performance;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;

import org.apache.log4j.Logger;

import edu.kit.ksri.db.mturk.connectionhandlers.AbstractWorkerManager;
import edu.kit.ksri.db.mturk.connectionhandlers.WorkerManager;
import edu.kit.ksri.db.mturk.utils.FileUtils;
import edu.kit.ksri.db.mturk.utils.messaging.ResultOutputCollector;
import edu.kit.ksri.db.mturk.var.StaticInformation;
import edu.kit.ksri.db.mturk.workerpools.AbstractWorker;
import edu.kit.ksri.db.mturk.workerpools.PoolManager;
import edu.kit.ksri.db.mturk.workerpools.WorkerPool;

/**
 * Copyright (c) 2010-2011 Karlsruhe Institute of Technology (KIT), Germany
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 * 
 * --------------------------------------------------------------------------
 * 
 * Software: CSP/WMV tool for dynamic quality management of microtasks
 * http://www.ksri.kit.edu/Upload/Users/PeopleClouds/HCOMP2011_An_extendable_toolkit_for_managing_quality_of_human-based_eServices.pdf
 * 
 * Organization: Karlsruhe Institute of Technology (KIT), Germany
 * http://www.kit.edu
 * 
 * Project: PeopleClouds project of the Karlsruhe Service Research Institute(KSRI)
 * http://www.ksri.kit.edu/Default.aspx?PageId=818
 * 
 * Contributing authors: David Bermbach, Sandra Rath, Pascal Wichmann, Robert Kern
 * 
 * ---------------------------------------------------------------------------
 * 
 * Purpose: This class is responsible for updating the settings on mturk. it periodically
 * polls for information updates and subsequently invokes the corresponding
 * forecast class. <br>
 * <br>
 * <b>Note:</b> This class expects the existance of the {@link WorkerPool}s
 * ACTIVE, PENDING, BLOCKED and UNKNOWN.
 */
public class TimeConstraintsManager implements Runnable {

	private static Logger basiclog = Logger.getLogger("basic." + TimeConstraintsManager.class);
	
	private static final Logger perflog = Logger.getLogger("perf." + TimeConstraintsManager.class);

	protected static final DecimalFormat df = new DecimalFormat("#,###,##0.0");

	/** the point in time at which all HITs should be terminated */
	protected Date deadline = null;

	/** what kind of forecast (i.e. linear) shall be used. */
	protected AbstractForecast forecastType = null;

	/** how many seconds shall elapse between shapshots */
	protected int secondsBetweenSnapshots = StaticInformation.secondsBetweenSnapshots;

	/** how many workers have to do the same task * number of hits */
	protected int numberOfAssignments = 1;

	/** workers which are allowed to work on this HIT */
	protected WorkerPool activePool;

	/** workers we know that are not allowed to work on this HIT */
	protected WorkerPool pendingPool;

	/** workers that will never again be allowed to work on any HIT */
	protected WorkerPool blockedPool;

	/** workers we don't know yet */
	protected WorkerPool unknownPool;

	/** The snapshots which have been taken, i.e. progress statistics */
	protected final List<ProgressSnapshot> snapshots = new ArrayList<ProgressSnapshot>();

	/** estimated value of the work duration per assignment */
	protected final long estimatedMillisPerAssignment;

	/**
	 * the {@link TimeConstraintsManager} will be running while this variable is
	 * true
	 */
	private static boolean running = true;

	/** this field will be true as soon as the run method has terminated. */
	private static boolean terminated = false;

	/**
	 * 
	 * @param deadline
	 * @param forecastType
	 *            the type of forecast (e.g. linear) which shall be used
	 * @throws Exception
	 *             whenever one of the worker pools could not be found.
	 */
	@Deprecated
	public TimeConstraintsManager(final Date deadline,
			final AbstractForecast forecastType,
			final int estimatedMillisPerAssignment) throws Exception {
		super();
		this.deadline = deadline;
		this.forecastType = forecastType;
		this.estimatedMillisPerAssignment = estimatedMillisPerAssignment;
		this.activePool = PoolManager.getInstance().getPoolForName("ACTIVE");
		this.pendingPool = PoolManager.getInstance().getPoolForName("PENDING");
		this.blockedPool = PoolManager.getInstance().getPoolForName("BLOCKED");
		this.unknownPool = PoolManager.getInstance().getPoolForName("UNKNOWN");
		if (this.activePool == null || this.pendingPool == null
				|| this.blockedPool == null || this.unknownPool == null) {
			throw new Exception("Please initialize the worker pools ACTIVE,"
					+ " PENDING, BLOCKED and UNKNOWN first!");
		}

	}

	/**
	 * uses the forecast class defined in {@link StaticInformation}.
	 * 
	 * @param deadline
	 * 
	 * @throws Exception
	 *             whenever one of the worker pools could not be found.
	 */
	public TimeConstraintsManager(final Date deadline,
			final int estimatedMillisPerAssignment) throws Exception {
		super();
		this.deadline = deadline;
		this.forecastType = StaticInformation.forecastClass.newInstance();
		this.estimatedMillisPerAssignment = estimatedMillisPerAssignment;
		this.activePool = PoolManager.getInstance().getPoolForName("ACTIVE");
		this.pendingPool = PoolManager.getInstance().getPoolForName("PENDING");
		this.blockedPool = PoolManager.getInstance().getPoolForName("BLOCKED");
		this.unknownPool = PoolManager.getInstance().getPoolForName("UNKNOWN");
		if (this.activePool == null || this.pendingPool == null
				|| this.blockedPool == null || this.unknownPool == null) {
			throw new Exception("Please initialize the worker pools ACTIVE,"
					+ " PENDING, BLOCKED and UNKNOWN first!");
		}

	}

	public void run() {
		ProgressSnapshot lastsnapshot = new ProgressSnapshot(), newsnapshot;
		lastsnapshot.setActivePoolSize(activePool.size());
		lastsnapshot.setAssignmentsCompletedPerMinute(activePool.size()
				* 60000.0 / estimatedMillisPerAssignment);
		lastsnapshot.setNumberOfAssignmentsCompleted(0);
		lastsnapshot.setNumberOfDistinctWorkers(0);
		lastsnapshot.setSnapshotDate(new Date());
		synchronized (snapshots) {
			snapshots.add(lastsnapshot);
		}

		while (running) {
			// wait before polling
			try {
				Thread.sleep(1000 * secondsBetweenSnapshots);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// create new snapshot and calculate / retrieve data
			newsnapshot = new ProgressSnapshot();
			synchronized (snapshots) {
				snapshots.add(newsnapshot);
			}
			int completedSinceLastSnapshot = ResultOutputCollector
					.getInstance().getCompletedAssignments()
					- lastsnapshot.getNumberOfAssignmentsCompleted();
			if (completedSinceLastSnapshot == 0) {
				// nothing has happened during the last period, to be on the
				// safe side increase
				// active pool size by one
				this.updateActivePool(activePool.size() + 1);
			}
			this.numberOfAssignments = ResultOutputCollector.getInstance()
					.getRequestedAssignments();
			double perMinute = completedSinceLastSnapshot
					/ (secondsBetweenSnapshots / 60.0);
			int total = lastsnapshot.getNumberOfAssignmentsCompleted()
					+ completedSinceLastSnapshot;
			double percentage = ((100 * total) / (this.numberOfAssignments));
			Date snapshotDate = new Date();
			// update object
			newsnapshot.setActivePoolSize(activePool.size());
			newsnapshot.setAssignmentsCompletedPerMinute(perMinute);
			newsnapshot.setNumberOfAssignmentsCompleted(total);
			newsnapshot.setNumberOfDistinctWorkers(activePool.size()); // FIXME
			// sense?
			newsnapshot.setSnapshotDate(new Date());
			// log and invoke update for pool sizes
			perflog.info(snapshotDate + ": " + total + " assignments completed ("
					+ df.format(percentage) + "%)");
			forecastType.updateActivePoolSize(this);
			lastsnapshot = newsnapshot;
			if (snapshotDate.after(deadline) || numberOfAssignments <= total)
				break;
		}
		basiclog.info("Exporting results...");
		if (exportResults())
			basiclog.info("Exporting results successful.");
		else
			basiclog.info("Error while exporting results.");
		basiclog.info("Terminated.");
		terminated = true;
	}

	/**
	 * exports all snapshots as a csv file
	 * 
	 * @return true if no error occurred
	 */
	private boolean exportResults() {
		StringBuffer export = new StringBuffer(ProgressSnapshot.getCSVHeaders());
		for (ProgressSnapshot p : snapshots) {
			export.append("\n"+p.getCSVString());
		}
		return FileUtils.writeToFile(export.toString(),
				StaticInformation.resultOutputSnapshots);
	}

	/**
	 * @return the deadline
	 */
	public Date getDeadline() {
		return this.deadline;
	}

	/**
	 * @param deadline
	 *            the deadline to set
	 */
	public void setDeadline(Date deadline) {
		this.deadline = deadline;
	}

	/**
	 * @return the forecastType
	 */
	public AbstractForecast getForecastType() {
		return this.forecastType;
	}

	/**
	 * @param forecastType
	 *            the forecastType to set
	 */
	public void setForecastType(AbstractForecast forecastType) {
		this.forecastType = forecastType;
	}

	/**
	 * @return the numberOfAssignmentsPerHIT
	 */
	public int getNumberOfAssignments() {
		return this.numberOfAssignments;
	}

	/**
	 * @return the activePool size
	 */
	public int getActivePoolSize() {
		return this.activePool.size();
	}

	/**
	 * @return the pendingPool size
	 */
	public int getPendingPoolSize() {
		return this.pendingPool.size();
	}

	/**
	 * @return the blockedPool size
	 */
	public int getBlockedPoolSize() {
		return this.blockedPool.size();
	}

	/**
	 * @return the unknownPool size
	 */
	public int getUnknownPoolSize() {
		return this.unknownPool.size();
	}

	/**
	 * @return the snapshots
	 */
	public List<ProgressSnapshot> getSnapshots() {
		return this.snapshots;
	}

	/**
	 * moves elements from the pending {@link WorkerPool} to the active
	 * workerpool (and the other way around) until one of the pools is empty or
	 * the number specified by the parameter has been reached.
	 * 
	 * @param newActivePoolSize
	 */
	public void updateActivePool(int newActivePoolSize) {
		int numberOfWorkers = newActivePoolSize - activePool.size();
		HashSet<AbstractWorker> set = pendingPool.getBestWorkers(numberOfWorkers);
		activePool.addWorkers(set);
		pendingPool.removeWorkers(set);
		perflog.info("Moved " + set.size() + " workers from pool"
				+ " \"PENDING\" to pool \"ACTIVE\"");
		perflog.info("Pool Sizes: ACTIVE (" + activePool.size() + "), PENDING ("
				+ pendingPool.size() + "), BLOCKED (" + blockedPool.size()
				+ ").");
		AbstractWorkerManager wm = WorkerManager.getInstance();
		for (AbstractWorker w : set) {
			String id = w.getWorkerId();
			Integer score = wm.getQualificationValue(id);
			if (score != null
					&& score == StaticInformation.activePoolQualificationValue) {
				perflog.info("Worker " + id + " already has the correct value.");
				continue;
			}
			wm.setQualificationValue(StaticInformation.scenario.getQualificationTypeID(), id,
					StaticInformation.activePoolQualificationValue);
		}
	}

	/**
	 * creates an csv output from the stored snapshots
	 */
	protected void logSnapshotsAsCSV() {
		String output = "\"Time\";" + "\"Number of Assignments Completed\";"
				+ "\"Active Pool Size\";"
				+ "\"Assignments Completed Per Minute\";"
				+ "\"Number of Distinct Workers\";" + "\"Total Assignments\";"
				+ "\"Deadline\"";
		synchronized (snapshots) {
			for (ProgressSnapshot p : snapshots) {
				output += "\n\"" + p.getSnapshotDate() + "\";" + "\""
						+ p.getNumberOfAssignmentsCompleted() + "\";" + "\""
						+ p.getActivePoolSize() + "\";" + "\""
						+ p.getAssignmentsCompletedPerMinute() + "\";" + "\""
						+ p.getNumberOfDistinctWorkers() + "\";" + "\""
						+ (this.numberOfAssignments) + "\";" + "\""
						+ this.deadline + "\"";
			}
		}
		try {
			PrintWriter pw = new PrintWriter(new File("log_" + new Date()
					+ ".csv"));
			pw.print(output);
			pw.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}

	}

	/**
	 * @return the running
	 */
	public static boolean isRunning() {
		return running;
	}

	/**
	 * @param running
	 *            the running to set
	 */
	public static void setRunning(boolean running) {
		TimeConstraintsManager.running = running;
	}

	/**
	 * @return the terminated
	 */
	public static boolean isTerminated() {
		return terminated;
	}

	/**
	 * @param terminated
	 *            the terminated to set
	 */
	public static void setTerminated(boolean terminated) {
		TimeConstraintsManager.terminated = terminated;
	}

}
